<?php

namespace liketp\database\concern;

use Closure;
use liketp\database\Query;
use liketp\database\Raw;
use liketp\exception\Exception;

/**
 * 查询器的设置查询条件的相关方法
 * 主要是 where 以及衍生方法
 * 查询器查询选项(options)中查询条件(where)的数据结构如下：
 * where = [
 *      logic    => [and | or]                   ,// 连接逻辑
 *      field    => [string | raw | query]       ,// 字段，可以是字段名、原生语句片段、查询器
 *      operate  => [= | > | < | in | between...],// 操作符号
 *      value    => [pure_value | raw | query]   ,// 值，可以是单纯的值、原生语句片段、查询器
 *      addition => [                            ,// 操作符合的附加信息
 *                      opposite => true | false ,// 操作是否需要取反，此元素值自行生成，无需用户传入
 *                      operate  => [>, =, between ...],// 附加操作，用于时间查询
 *                  ]               
 * ]
 */
trait WhereQuery
{
    protected $operatesMap = [
        'eq' => '=',
        'gt' => '>',
        'lt' => '<',
        'egt' => '>=',
        'elt' => '<=',
        'neq' => '<>',
        'like' => 'like',

        'in' => 'in',
        'not in' => 'in',
        'between' => 'between',
        'not between' => 'between',
        'null' => 'null',
        'not null' => 'null',

        'exists' => 'exists',
        'not exists' => 'exists',

        'exp' => 'exp',
        'column' => 'column',

        '= time' => 'time',
        '> time' => 'time',
        '< time' => 'time',
        '>= time' => 'time',
        '<= time' => 'time',
        '<> time' => 'time',
        'between time' => 'time',
        'not between time' => 'time'
    ];

    protected $logic = [
        'and' => ['&', '&&', 'AND'],
        'or' => ['|', '||', 'OR'],
        'xor' => ['^', 'XOR']
    ];

    public function where($field = null, $operate = null, $value = null, $logic = 'and')
    {
        $argsNum = func_num_args();

        //where(null)
        if (is_null($field)) {
            return $this;
        }
        //where(function(){}) | where(function(){}, 'OR')
        else if ($field instanceof Closure) {
            $logic = is_null($operate) ? 'and' : $operate;
            $this->assembleWhere($field, 'closure', null, $logic);
        }
        //where(query) | where(query, 'AND')
        else if ($field instanceof Query) {
            if ($field === $this) throw new Exception('can\'t accept Query instance');
            $logic = is_null($operate) ? 'and' : $operate;
            $this->assembleWhere($field, 'query', null, $logic);
        }
        //raw
        else if ($field instanceof Raw) {
            $logic = is_null($operate) ? 'and' : $operate;
            $this->assembleWhere($field, 'raw', null, $logic);
        }
        //where([city=>zhengzhou, age=>['>', 24], [travel_by, motorcycle], _logic=>or])
        else if (is_array($field)) {
            foreach ($field as $k => $v) {
                if (is_array($v)) {
                    //where([age=>['>', 24]])
                    if (is_string($k)) {
                        $this->where($k, ...$v);
                    } else {
                        $firstKey = array_key_first($v);
                        //where([[age=>['>', 24]], [gender => male]])
                        if (is_string($firstKey)) {
                            $this->where($firstKey, ...(array)$v[$firstKey]);
                        }
                        //where([[age, '>', 24], [gender, 'in', [male, female]]])
                        else {
                            $this->where(...$v);
                        }
                    }
                }
                //where([city=>zhengzhou, career=>coder])
                else {
                    $this->where($k, '=', $v);
                }
            }
        }
        //where(id, 5)
        else if ($argsNum == 2) {

            $logic = 'and';
            $value = $operate;
            $operate = '=';
            $this->assembleWhere($field, $operate, $value, $logic);
        } else if ($argsNum == 3) {

            //where(age, 24, and)
            if ($this->getAndCheckOperate($operate) === false) {
                $logic = $value;
                $value = $operate;
                $operate = '=';
                $addition = [];
            }
            //where(state, '>', 3)
            else {
                [$operate, $addition] = $this->getAndCheckOperate($operate);
                $logic = 'and';
            }
            $this->assembleWhere($field, $operate, $value, $logic, $addition);
        }
        //where(name, like, zhang%, or)
        else if ($argsNum == 4) {

            [$operate, $addition] = $this->getAndCheckOperate($operate, true);
            $this->assembleWhere($field, $operate, $value, $logic, $addition);
        }
        return $this;
    }

    public function whereOr($field, $operate = null, $value = null)
    {
        if (func_num_args() != 3) {
            $value = $operate;
            $operate = '=';
        }
        return $this->where($field, $operate, $value, 'OR');
    }

    public function whereXor($field, $operate = null, $value = null)
    {
        if (func_num_args() != 3) {
            $value = $operate;
            $operate = '=';
        }
        return $this->where($field, $operate, $value, 'XOR');
    }

    public function whereRaw(string $sqlPartment, $bindData = [], $logic = 'AND')
    {
        if ($this->isLogic($bindData)) {
            $logic = $bindData;
            $bindData = [];
        }
        $raw = new Raw($sqlPartment, $bindData);

        return $this->where($raw, 'raw', null, $logic);
    }

    public function whereNull($field, $logic = 'AND')
    {
        return $this->where($field, 'null', null, $logic);
    }

    public function whereNotNull($field, $logic = 'AND')
    {
        return $this->where($field, 'not null', null, $logic);
    }

    public function whereIn($field, $value, $logic = 'AND')
    {
        return $this->where($field, 'in', $value, $logic);
    }

    public function whereNotIn($field, $value, $logic = 'AND')
    {
        return $this->where($field, 'not in', $value, $logic);
    }

    public function whereBetween($field, $value, $logic = 'AND')
    {
        return $this->where($field, 'between', $value, $logic);
    }

    public function whereNotBetween($field, $value, $logic = 'AND')
    {
        return $this->where($field, 'not between', $value, $logic);
    }

    public function whereLike($field, $value, $logic = 'AND')
    {
        return $this->where($field, 'like', $value, $logic);
    }

    public function whereNotLike($field, $value, $logic = 'AND')
    {
        return $this->where($field, 'not like', $value, $logic);
    }

    public function whereExists($condition, $logic = 'AND')
    {
        return $this->where($condition, 'exists', null, $logic);
    }

    public function whereNotExists($condition, $logic = 'AND')
    {
        return $this->where($condition, 'not exists', null, $logic);
    }

    public function whereExp($field, $exp, $logic = 'AND')
    {
        return $this->where($field, 'exp', $exp, $logic);
    }

    public function whereColumn($field1, $operate, $field2 = null, $logic = 'AND')
    {
        if (is_null($field2)) {
            $field2 = $operate;
            $operate = '=';
        }
        return $this->where($field1, 'column', [$operate, $field2], $logic);
    }


    //------------------------------------------------ protected
    protected function logic($logic)
    {
        if (is_string($logic)) {
            $logic = strtoupper($logic);
            if (in_array($logic, $this->logic['or'])) return 'OR';
            if (in_array($logic, $this->logic['xor'])) return 'XOR';
        }
        return 'AND';
    }

    protected function isLogic($logic): bool
    {
        if (is_string($logic)) {
            $logicSymbols = array_merge(...array_values($this->logic));
            return in_array(strtoupper($logic), $logicSymbols);
        } else {
            return false;
        }
    }

    protected function getAndCheckOperate($operate, bool $throwException = false)
    {
        if (isset($this->operatesMap[$operate])) {

            $symbol = $this->operatesMap[$operate];
            $opposite = substr($operate, 0, 4) == 'not ';

            if ($symbol == 'time') {
                $timeOpe = substr($operate, -1 * strlen(' time'));
                $timeOpe = $opposite ? substr($timeOpe, strlen('not ')) : $timeOpe;
                $additionOperate = trim($timeOpe);
            } else {
                $additionOperate = '';
            }

            return [$symbol, ['opposite' => $opposite, 'operate' => $additionOperate]];
        } else if (in_array($operate, $this->operatesMap)) {

            return [$operate, ['opposite' => false, 'operate' => '']];
        }

        if ($throwException) throw new Exception('query operate is not allowed');
        return false;
    }

    protected function assembleWhere($field, $operate, $value, $logic, $addition = [])
    {
        $whereItem = [
            'logic' => $this->logic($logic),
            'field' => $field,
            'operate' => $operate,
            'value' => $value,
            'addition' => $addition
        ];
        $this->setOption('where', $whereItem, true);
    }
}
